package jia;

import jason.environment.grid.Location;

import java.awt.Point;
import java.util.ArrayList;

import prop.PropertiesReader;

import arch.CowboyArch;
import arch.LocalWorldModel;
import env.WorldModel;

public class SwitchFinder {	
	LocalWorldModel model;

	public SwitchFinder(LocalWorldModel model) {
		this.model = model;
	}

	public Point getSwitch(Point agent, String agentName) {
		Point button = openFence(agent);
		
		// if a switch which should be triggered exists, and this agent
		// should open it
		if (button != null) {			
			// get switch target
			Point target = getSwitchTarget(agent, button, true);
			Point otherSideOfSwitch = getSwitchTarget(agent, button, false);
			
			// is there already an agent heading this way?
			for (int i = 0; i < model.targets.length; i++) {
				if (model.targets[i] == null)
					continue;
				// a target is already there
				if (model.targets[i].x == target.x
						&& model.targets[i].y == target.y) {
					if (i == CowboyArch.getAgId(agentName)) { // this agent has
						// the target
						// already, use
						// same target
						model.setTargetAgentPos(CowboyArch.getAgId(agentName),
								target.x, target.y);
						
						return new Point(target.x, target.y);
					} else {// other agent is responsible
						return null;
					}
				} else if (model.targets[i].x == otherSideOfSwitch.x && model.targets[i].y == otherSideOfSwitch.y){
					return null;
				}
			}
			if (model.hasObject(WorldModel.AGENT, target.x, target.y)) {
				if(model.getAgPos(CowboyArch.getAgId(agentName)).x != target.x && model.getAgPos(CowboyArch.getAgId(agentName)).y != target.y) {
					return null;
				}
			}

			// return target
			model.setTargetAgentPos(CowboyArch.getAgId(agentName), target.x,
					target.y);
			return new Point(target.x, target.y);
		} else {
			// otherwise, return false, so the agent will get another target
			return null;
		}
	}

	/**
	 * Determines if a fence should be opened, and which switch can do it.
	 * 
	 * @param agent
	 * @param button
	 * @return Coordinates of the switch which opens the fence or null if no
	 *         switch should be triggered
	 */
	private Point openFence(Point agent) {
		// get the nearest switch
		Point nearestSwitch = null;
		double dist = Double.MAX_VALUE;
		for (int i = 0; i < model.getWidth(); i++) {
			for (int j = 0; j < model.getHeight(); j++) {
				if (model.hasObject(WorldModel.SWITCH, i, j)) {
					Point tmpSwitch = new Point(i, j);
					double tmpDist = agent.distance(tmpSwitch);
					if (tmpDist < dist) {
						nearestSwitch = tmpSwitch;
						dist = tmpDist;
					}
				}
			}
		}

		// no switch is known - return null
		if (nearestSwitch == null) {
			return null;
		}

		// find fence start and end (start being near the switch)
		Point[] fencePoints = getFence(nearestSwitch);
		if (fencePoints == null)
			return null;

		Point fenceStart = fencePoints[0];
		Point fenceEnd = fencePoints[1];

		int fenceRange = PropertiesReader.getInstance().getIntegerProperty("fence_range");
		int cowsNeeded = PropertiesReader.getInstance().getIntegerProperty("cows_needed");
		
		// if cows are near the fence, or an agent has a target on the other
		// side of a fence - return the switch
		if (cowsNearFence(fenceStart, fenceEnd, fenceRange, cowsNeeded)
				|| agentPassingThrough(fenceStart, fenceEnd, fenceRange)) {
			return nearestSwitch;
		}

		// no one needs this fence to be opened
		return null;
	}

	/**
	 * Determines whether any cows are near the fence. This is the case if any
	 * cow exists in front of the fence within the specified range.
	 * 
	 * @param fenceStart
	 *            Startpoint of the fence
	 * @param fenceEnd
	 *            Endpoint of the fence
	 * @param range
	 *            Max distance the cows can be from the fence
	 * @param numberOfCowsNeeded
	 *            Number of cows that must be in front of the fence, before it
	 *            should be opened
	 * @return Whether cows are near the fence
	 */
	private boolean cowsNearFence(Point fenceStart, Point fenceEnd, int range,
			int numberOfCowsNeeded) {
		// determine area points (upper left and lower right)
		Point[] area = determineArea(fenceStart, fenceEnd, range);
		Point upperLeft = area[0];
		Point lowerRight = area[1];

		int cowsFound = 0;
		for (int i = upperLeft.x; i < lowerRight.x; i++) {
			for (int j = upperLeft.y; j < lowerRight.y; j++) {
				if (model.hasObject(WorldModel.COW, i, j)) {
					cowsFound++;

					// enough cows found
					if (cowsFound >= numberOfCowsNeeded) {
						return true;
					}
				}
			}
		}

		// not enough cows found
		return false;
	}

	/**
	 * Determines whether an agent at one side of a fence has a target on the
	 * other side. If this is the case, he will need someone to open the fence.
	 * 
	 * @param fenceStart
	 *            Startpoint of the fence
	 * @param fenceEnd
	 *            Endpoint of the fence
	 * @param range
	 *            How close to the fence must the agent be, before he will be
	 *            considered
	 * @return Whether an agent is trying to pass through
	 */
	private boolean agentPassingThrough(Point fenceStart, Point fenceEnd,
			int range) {

		for(int i = 0; i < WorldModel.agsByTeam; i++) {
			int agent = i;
			Location agentPos = model.getAgPos(agent);
			Location target = model.getTargetAgentPos(agent);
			Location fencePos; // the point in the fence directly opposite the agent
			
			if (fenceStart.x == fenceEnd.x) { // vertical fence
				fencePos = new Location(fenceStart.x, agentPos.y);
			} else {
				fencePos = new Location(agentPos.x, fenceStart.y);
			}

			if (target == null || agentPos == null || fencePos == null)
				continue;
			// if the fence directly next to the agent is closer to the target,
			// the target is probably on the other side of the fence = agent
			// must pass through
			
			if (agentPos.distance(target) > fencePos.distance(target)) {
				return true;
			}
		}

		return false;
	}

	private Point[] determineArea(Point fenceStart, Point fenceEnd, int range) {
		// yS(tart) must be less than yE(nd)
		Point upperLeft, lowerRight;
		if (fenceStart.x == fenceEnd.x) { // vertical
			int yS, yE;
			if (fenceStart.y < fenceEnd.y) {
				yS = fenceStart.y;
				yE = fenceEnd.y;
			} else {
				yS = fenceEnd.y;
				yE = fenceStart.y;
			}
			upperLeft = new Point(fenceStart.x - range, yS);
			lowerRight = new Point(fenceEnd.x + range, yE);
		} else { // horizontal
			// xS(tart) must be less than xE(nd)
			int xS, xE;
			if (fenceStart.x < fenceEnd.x) {
				xS = fenceStart.x;
				xE = fenceEnd.x;
			} else {
				xS = fenceEnd.x;
				xE = fenceStart.x;
			}
			upperLeft = new Point(xS, fenceStart.y - range);
			lowerRight = new Point(xE, fenceEnd.y + range);
		}
		return new Point[] { upperLeft, lowerRight };
	}

	private Point[] getFence(Point nearestSwitch) {
		Point fenceStart, fenceEnd;
		if (model.hasObject(WorldModel.FENCE, nearestSwitch.x,
				nearestSwitch.y - 1)) {
			// going north
			fenceStart = new Point(nearestSwitch.x, nearestSwitch.y - 1);
			int x = fenceStart.x;
			int y = fenceStart.y;
			while (model.hasObject(WorldModel.FENCE, x, y)) {
				y -= 1;
			}
			fenceEnd = new Point(x, y + 1);
		} else if (model.hasObject(WorldModel.FENCE, nearestSwitch.x,
				nearestSwitch.y + 1)) {
			// going south
			fenceStart = new Point(nearestSwitch.x, nearestSwitch.y + 1);
			int x = fenceStart.x;
			int y = fenceStart.y;
			while (model.hasObject(WorldModel.FENCE, x, y)) {
				y += 1;
			}
			fenceEnd = new Point(x, y - 1);
		} else if (model.hasObject(WorldModel.FENCE, nearestSwitch.x - 1,
				nearestSwitch.y)) {
			// going west
			fenceStart = new Point(nearestSwitch.x - 1, nearestSwitch.y);
			int x = fenceStart.x;
			int y = fenceStart.y;
			while (model.hasObject(WorldModel.FENCE, x, y)) {
				x -= 1;
			}
			fenceEnd = new Point(x + 1, y);
		} else if (model.hasObject(WorldModel.FENCE, nearestSwitch.x + 1,
				nearestSwitch.y)) {
			// going east
			fenceStart = new Point(nearestSwitch.x + 1, nearestSwitch.y);
			int x = fenceStart.x;
			int y = fenceStart.y;
			while (model.hasObject(WorldModel.FENCE, x, y)) {
				x += 1;
			}
			fenceEnd = new Point(x - 1, y);
		} else {
			// fence is not known
			return null;
		}
		return new Point[] { fenceStart, fenceEnd };
	}

	/**
	 * Returns the target next to the switch, which the agent must stand on, in
	 * order to trigger switch.
	 * 
	 * @param agent
	 *            Agent coordinates
	 * @param button
	 *            Switch coordinates
	 * @return Coordinates the agent should go to
	 */
	private Point getSwitchTarget(Point agent, Point button, boolean closest) {
		Point target = new Point();

		if (model.hasObject(WorldModel.FENCE, button.x + 1, button.y)
				|| model.hasObject(WorldModel.FENCE, button.x - 1, button.y)) { // horizontal
			// fence
			if (agent.y < button.y) { // agent is belove switch
				target.x = button.x;
				if(closest) {
					target.y = button.y - 1;
				} else {
					target.y = button.y + 1;
				}
			} else { // agent is above switch
				target.x = button.x;
				if (closest) {
					target.y = button.y + 1;
				} else {
					target.y = button.y - 1;
				}
			}
		} else if (model.hasObject(WorldModel.FENCE, button.x, button.y + 1)
				|| model.hasObject(WorldModel.FENCE, button.x, button.y - 1)) { // vertical
			// fence
			if (agent.x < button.x) { // agent is west of switch
				if(closest) {
					target.x = button.x - 1;
				} else {
					target.x = button.x + 1;
				}
				target.y = button.y;
			} else {
				if(closest) {
					target.x = button.x + 1;
				} else {
					target.x = button.x - 1;
				}
				target.y = button.y;
			}
		}

		return target;
	}
}
